package dev;

/**
 * An EventScope defines a period for which events are active.
 * TODO:
	- spread functionality over multiple classes for "using"
	- create tweening and animation sequencing classes
	- emulate CSS transitions using this class
	- use propertychange event in IE when monitoring HTMLElement properties
	  see if using watch, Object.defineProperty or Object.prototype.__defineSetter__ can also be used
	  https://developer.mozilla.org/en/Core_JavaScript_1.5_Guide/Working_with_Objects#Defining_Getters_and_Setters
 * @author Cref
 */
#if !macro
import hxtc.events.ClosureEventDispatcher;
import hxtc.Tools;
import jstm.Host;

using hxtc.Tools;
#end
class EventScope {
	#if !macro
	//TODO: replace by public function new(w:Window) ?
	public static var global = {
		var r = new EventScope();
		r.setActive(true);
		r;
	}
	
	function new() {
		timeouts = new IntHash();
		intervals = [];
		animations = [];
		scopes = [];
		listeners = [];
		onstarts = [];
		onends = [];
	}
	
	public function createEventScope(activator:(Bool->Void)->Void) {
		var r = new EventScope();
		activator(r.setActive);
		scopes.push(r);
		if (!active) r.setActive(false);
		return r;
	}
	
	var scopes:Array<EventScope>;
	var timeouts:IntHash<Void>;
	var onstarts:Array<Void->Void>;
	var onends:Array<Void->Void>;
	var intervals:Array<Arguments<Dynamic>>;
	var animations:Array<Animation>;
	var listeners:Array<Arguments<Dynamic>>;
	public var active(default,null):Bool;
	function setActive(b:Bool):Void {
		if (active == b) return;
		for (i in b?onstarts:onends) i();
		var ar = ((active = b)?'add':'remove') + 'EventListener';
		for (l in listeners) l[0][cast ar](l[1], l[2], false);
		b
			?for (i in intervals) i.interval=Host.window.setInterval(i[1], i[0])
			:for (i in intervals) Host.window.clearInterval(i.interval)
		;
		b
			?for (a in animations) a.start()
			:for (a in animations) a.stop()
		;
		if (!b) {
			for (i in timeouts.keys()) Host.window.clearTimeout(i);
			for (s in scopes) if (s.active) s.setActive(false);
		}
	}
	
	public inline function before(fn1:Dynamic, fn2) return bind(fn1, fn2, true)
	public inline function after(fn1:Dynamic, fn2) return bind(fn1, fn2)
	function bind(fn1, fn2,?doBefore:Bool) {
		var c:Closure = cast fn1;
		var evtId = c.scope.getInstanceId() + '.' + c._name,fn=c.scope[cast c._name];
		if (!fn._isDispatcher) {
			//wrap the function in an event dispatcher
			var dispatcher = function() {
				//still considering on whether to pass on the function arguments and allowing stopPropagation
				if (!Host.window.dispatchEvent(createEvent('b'+evtId))) return;
				var r = fn.apply(c.scope, ES5.arguments);
				//still considering on whether to pass on the function arguments and return value
				Host.window.dispatchEvent(createEvent('a'+evtId));
				return r;
			};
			untyped dispatcher._isDispatcher = true;
			c.scope[cast c._name] = dispatcher;
		}
		on(Host.window, (doBefore?'b':'a') + evtId, fn2);
		return this;
	}
	
	static function createEvent(type:String):Dynamic {
		var event = Host.window.document.createEvent('Event');
		event.initEvent(type, false, true);
		return event;
	}
	
	//whenever any of the trigger functions is called, a timeout will start/reset.
	//once the timeout expires, fn will be called.
	//this method is meant for doing visual updates or vice versa.
	public function updateUI(triggers:Array<Dynamic>, fn:Void->Void, callLimitTimeframe:Int = 100) {
		var dfn = delayed(fn, callLimitTimeframe, true);
		for (t in triggers) after(t, dfn);
		return this;
	}
	
	public function updater(fn:Void->Void, callLimitTimeframe:Int = 100) {
		var dfn = delayed(fn, callLimitTimeframe, true),t=this;
		var binder:Binder = null;
		binder={
			bind:function(fn:Dynamic) {
				t.after(fn, dfn);
				return binder;
			}
		};
		return binder;
	}
	
	public function sync<T>(p1:Property<T>, p2:Property<T>) {
		after(p1.change,function(_) p2.value = p1.value);
		after(p2.change,function(_) p1.value = p2.value);
	}
	
	/*
	//can not support writes using array index
	public function watchArray(arr:Array<Dynamic>,refresh:Void->Void) {
		updateUI([
			arr.pop,
			arr.push,
			arr.shift,
			arr.unshift,
			arr.sort,
			arr.reverse,
			arr.splice
		], refresh);
	}*/
	
	public function on(obj:org.w3c.dom.events.EventTarget, type:String, listener:Dynamic->Void) {
		listeners.push(ES5.arguments);
		if (active) obj.addEventListener(type, listener, false);
		return this;
	}
	
	public function onStart(fn:Void->Void) {
		onstarts.push(fn);
		if (active) fn();
		return this;
	}
	
	public function onEnd(fn:Void->Void) {
		onends.push(fn);
		return this;
	}
	
	public function onSwitch(swFn:Bool->Void) return onStart(function()swFn(true)).onEnd(function()swFn(false))
	
	//asCallLimitTimeframe=true uses msec as a timeframe in which fn shall only be called once
	public function delayed(fn:Dynamic, msec:Int,?asCallLimitTimeframe:Bool):Dynamic {
		var t = this,n = null;
		return function() {
			var a = ES5.arguments;
			if (asCallLimitTimeframe) Host.window.clearTimeout(n);
			n=Host.window.setTimeout(function() {
				t.timeouts.remove(n);
				fn.apply(t,a);
			},msec);
			t.timeouts.set(n,null);
		}
	}
	
	public function every(msec:Int, fn:Void->Void) {
		intervals.push(ES5.arguments);
		if (active) ES5.arguments.interval=Host.window.setInterval(fn, msec);
		return this;
	}
	
	public function animate(callbck:Void->Void, ?elm:HTMLElement) {
		var frame = null,ani={
			start:function() {
				callbck();
				frame = Host.window.requestAnimationFrame(cast ES5.arguments.callee, elm);
			},
			stop:function() Host.window.cancelRequestAnimationFrame(frame)
		};
		animations.push(ani);
		if (active) ani.start();
		return this;
	}

	//subs contains all objects that were created for this scope that need to be destroyed
	//var subs:Array < Scoped > ;
	//var propertyListeners:Hash < Void->Void > ;
	//public var updateTimeout:Int;

	//function _change(obj:Dynamic,key:String):Void->Void return propertyListeners.get(obj.id+'.'+key)

	//calls _change
	//@:macro public function change(eThis:Expr, property:Expr):Expr {}
	
	
	
	//TODO: add @:macro: createEvent(evtType:ExprRequire<EventType>):Expr
	//function _createEvent(type:String,?eventClass='Event'):Dynamic return element.ownerDocument.createEvent(eventClass)
	//TODO: add @:macro: createElement(elmClass:ExprRequire<HTMLElement>):Expr
	//function _createElement(tagName:String):Dynamic return element.ownerDocument.createElement(type)
	#end
}


typedef Closure = { scope:Dynamic, method:Function<Dynamic,Dynamic,Dynamic>, _name:String }
typedef Animation = { start:Void->Void, stop:Void->Void }
typedef Binder = { bind:Dynamic->Binder }
